# -*- coding: UTF-8 -*-
"""
Le système de cache de Django.

Ce module définit un ensemble de back-ends pour le cache; tous ces back-ends
se conforment à une API simple. En deux mots, un cache est une série de valeurs
(on entend ici par valeur tout objet pouvant être traité par le module Python
pickle); chaque valeur est identifiée par une chaîne (la clé). Pour une
description complète de l'API, voyez la description ci-dessous de l'objet
abstrait Cache.

En règle générale, le code client n'aura jamais besoin d'accéder de manière directe à un
back-end; il passera plutôt par la fonction get_cache(). Cette fonction
utilise le paramètre settings.CACHE_BACKEND pour créer et charger un objet cache
approprié.

Le paramètre CACHE_BACKEND est une pseudo-URI; quelques exemples :

    memcached://127.0.0.1:11211/     Back-end memcached; le serveur correspondant
                                     fonctionne en local sur le port 11211.

    pgsql://nom-table/               Back-end pgsql (le back-end pgsql utilise la
                                     même base de données et le même nom d'utilisateur
                                     que le reste du CMS; vous ne devez spécifier qu'un
                                     nom de table).

    file:///var/tmp/django.cache     Avec ce back-end, le cache est stocké sous forme de fichier;
                                     le fichier en question se trouve à l'emplacement
                                     /var/tmp/django.cache.

    simple:///                       Avec ce back-end simple mono-processus, le cache est
                                     stocké directement en mémoire; en règle générale, ce
                                     back-end ne servira qu'à des fins de test.
                                     Notez que ce back-end ne fonctionne PAS correctement
                                     avec des threads.

Tous ces back-ends acceptent des arguments; ils sont spécifiés dans la "query string"
de l'URI. Les arguments valides sont :

   timeout
       Délai d'expiration par défaut, en secondes, à utiliser pour le cache. Vaut
       par défaut 5 minutes (300 secondes).

   max_entries
       Pour les back-ends file, simple et pgsql, cet argument précise le nombre maximum
       d'entrées autorisées avant que le cache ne soit nettoyé. Vaut par défaut
       300.

   cull_percentage
       Le pourcentage d'entrées à effacer lorsque max_entries est atteint. En
       fait, ce pourcentage est une fraction de la forme 1/cull_percentage;
       écrivez donc cull_percentage=3 pour effacer un tiers des entrées
       lorsque max_entries est atteint.

       Une valeur de 0 signifie que le cache sera effacé dans sa totalité une
       fois max_entries atteint. Ceci rend *bien* plus rapide le processus de
       purgation du cache mais cela signifie aussi que le nombre de valeurs
       pouvant être obtenues directement à partir du cache diminuera.

À titre d'exemple :
    memcached://127.0.0.1:11211/?timeout=60
    pgsql://nom-table/?timeout=120&max_entries=500&cull_percentage=4

Les arguments invalides sont ignorés de manière silencieuse, tout comme le
fait de passer une valeur invalide à un argument connu.

Pour l'instant, seuls les back-ends memcached et simple ont été implémentés; des back-ends
utilisant postgres et un stockage sous forme de fichiers sont prévus.
"""

##############
# Exceptions #
##############

class InvalidCacheBackendError(Exception):
    pass

################################
# La classe de base, abstraite #
################################

class _Cache:

    def __init__(self, params):
        timeout = params.get('timeout', 300)
        try:
            timeout = int(timeout)
        except (ValueError, TypeError):
            timeout = 300
        self.default_timeout = timeout

    def get(self, key, default=None):
        '''
        Va chercher la valeur du cache correspondant à une clé donnée.  Si la
        clé n'existe pas, renvoie default, dont la valeur par défaut est None.
        '''
        raise NotImplementedError

    def set(self, key, value, timeout=None):
        '''
        Définit une valeur dans le cache.  Si timeout est spécifié, ce sera
        ce délai d'expiration qui sera associé à la clé; sinon, le délai
        d'expiration par défaut sera utilisé.
        '''
        raise NotImplementedError

    def delete(self, key):
        '''
        Supprime du cache la valeur correspondant à une clé donnée; en cas
        d'échec, se termine de manière silencieuse.
        '''
        raise NotImplementedError

    def get_many(self, keys):
        '''
        Va chercher plusieurs valeurs à la fois.  Pour certains back-ends (comme
        memcached ou pgsql), cette opération sera *bien* plus rapide que la recherche
        individuelle de chaque valeur.

        Renvoie un dictionnaire associant chaque clé de la liste keys à sa valeur
        correspondante.  Si l'une des clés spécifiée n'existe pas dans le cache,
        elle sera absente du dictionnaire final.
        '''
        d = {}
        for k in keys:
            val = self.get(k)
            if val is not None:
                d[k] = val
        return d

    def has_key(self, key):
        '''
        Renvoie True si la valeur correspondant à une clé donnée est
        présente dans le cache et n'a pas encore expiré.
        '''
        return self.get(key) is not None

######################
# Back-end memcached #
######################

try:
    import memcache
except ImportError:
    _MemcachedCache = None
else:
    class _MemcachedCache(_Cache):
        """Back-end memcached."""

        def __init__(self, server, params):
            _Cache.__init__(self, params)
            self._cache = memcache.Client([server])

        def get(self, key, default=None):
            val = self._cache.get(key)
            if val is None:
                return default
            else:
                return val

        def set(self, key, value, timeout=0):
            self._cache.set(key, value, timeout)

        def delete(self, key):
            self._cache.delete(key)

        def get_many(self, keys):
            return self._cache.get_multi(keys)

################################
# Cache mémoire mono-processus #
################################

import time

class _SimpleCache(_Cache):
    """Cache mémoire mono-processus simple."""

    def __init__(self, host, params):
        _Cache.__init__(self, params)
        self._cache = {}
        self._expire_info = {}

        max_entries = params.get('max_entries', 300)
        try:
            self._max_entries = int(max_entries)
        except (ValueError, TypeError):
            self._max_entries = 300

        cull_frequency = params.get('cull_frequency', 3)
        try:
            self._cull_frequency = int(cull_frequency)
        except (ValueError, TypeError):
            self._cull_frequency = 3

    def get(self, key, default=None):
        now = time.time()
        exp = self._expire_info.get(key, now)
        if exp is not None and exp < now:
            del self._cache[key]
            del self._expire_info[key]
            return default
        else:
            return self._cache.get(key, default)

    def set(self, key, value, timeout=None):
        if len(self._cache) >= self._max_entries:
            self._cull()
        if timeout is None:
            timeout = self.default_timeout
        self._cache[key] = value
        self._expire_info[key] = time.time() + timeout

    def delete(self, key):
        try:
            del self._cache[key]
        except KeyError:
            pass
        try:
            del self._expire_info[key]
        except KeyError:
            pass

    def has_key(self, key):
        return self._cache.has_key(key)

    def _cull(self):
        if self._cull_frequency == 0:
            self._cache.clear()
            self._expire_info.clear()
        else:
            doomed = [k for (i, k) in enumerate(self._cache) if i % self._cull_frequency == 0]
            for k in doomed:
                self.delete(k)

##############################################################
# Lecture des paramètres et chargement du back-end approprié #
##############################################################

from cgi import parse_qsl

_BACKENDS = {
    'memcached' : _MemcachedCache,
    'simple'    : _SimpleCache,
}

def get_cache(backend_uri):
    if backend_uri.find(':') == -1:
        raise InvalidCacheBackendError("L'URI d'un back-end doit commencer par backend://")
    scheme, rest = backend_uri.split(':', 1)
    if not rest.startswith('//'):
        raise InvalidCacheBackendError("L'URI d'un back-end doit commencer par backend://")
    if scheme not in _BACKENDS.keys():
        raise InvalidCacheBackendError("%r n'est pas le nom d'un back-end valide" % scheme)

    host = rest[2:]
    qpos = rest.find('?')
    if qpos != -1:
        params = dict(parse_qsl(rest[qpos+1:]))
        host = rest[:qpos]
    else:
        params = {}
    if host.endswith('/'):
        host = host[:-1]

    return _BACKENDS[scheme](host, params)

from django.conf.settings import CACHE_BACKEND
cache = get_cache(CACHE_BACKEND)
